package controller;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.StringTokenizer;
import org.apache.log4j.Logger;
import ssh.SSHCommandException;
import ssh.SSHConnectionHandler;
import utils.Command;
import utils.IllegalValueException;
import model.DistributedCommand;
import model.Hub;
import model.IPAddress;
import model.Link;
import model.NetworkInterface;
import model.Node;
import model.PhysicalNode;
import model.exception.EbtablesException;

/**
 * La classe Ebtables rappresenta il controller per la gestione della topologia. Essa permette 
 * di gestire le regole Ebtable tramite i metodi {@link TopologyEmulation#enforce()},
 * {@link TopologyEmulation#destroy()} e {@link TopologyEmulation#reset()}. La classe crea le regole
 * ebtables e le invia al server opportuno. Le informazionio sulla topologia di rete e sui server
 * vengono ottenute navigando la struttura dati partendo dalla classe Hub. 
 * 
 * La catena che ci permette di risalire dagli Hub alla topologia è la seguente: 
 * un Hub ha una lista {@link Link}; dai Link è possibile risalire alle {@link NetworkInterface};
 * dalle NetworkInterface è possibile risalire al {@link Node}; dai Node è possibile infine ricavare
 * i {@link PhysicalNode} che rappresentano i server del cluster ai quali inviare, tramite SSH
 * (utilizzando il gestore {@link SSHConnectionHandler}), i comandi ebtables.
 * 
 * 
 * @author Dumella De Rosa Gennaro
 *
 */
public class TopologyEmulation {
	
	private ArrayList<Hub> hubs;
	/*File per operazioni di Undo*/
	private File file;
	
	private static final Logger log = Logger.getLogger(TopologyEmulation.class);
	
	
	/**
	 * Costruisce un oggetto Ebtables con associata un {@link Hub}. Su tale oggetto è possibile
	 * eseguire i metodi {@link TopologyEmulation#enforce()}, {@link TopologyEmulation#destroy()} 
	 * e {@link TopologyEmulation#reset()} che avranno effetto su tutti i server fisici coninvolti 
	 * nella topologia determinata dall'hub.  
	 * 
	 * @param hub - Hub della topologia di rete
	 */
	public TopologyEmulation(Hub hub){
		hubs = new ArrayList<Hub>();
		this.hubs.add(hub);
		file = new File("src/undo.txt");
		log.debug("Create file src/undo.txt to Undo Operation");
	}
	
	/**
	 * Costruisce un oggetto Ebtables con associata una {@link ArrayList<{@link Hub}>. Su tale 
	 * oggetto è possibile eseguire i metodi {@link TopologyEmulation#enforce()}, 
	 * {@link TopologyEmulation#destroy()} e {@link TopologyEmulation#reset()} che avranno effetto 
	 * su tutti i server fisici coninvolti nella topologia determinata dalla lista di hub. 
	 * 
	 * @param hubs - Lista di Hub della topologia di rete
	 */
	public TopologyEmulation(ArrayList<Hub> hubs){
		this.hubs = hubs;
		file = new File("src/undo.txt");
		log.debug("Create file src/undo.txt to Undo Operation");
	}
	
	/**
	 * Crea la topologia descritta dalla lista di Hub inviando i comandi ebtables opportuni ai server
	 * fisici opportuni. Se solo un comando non viene eseguito correttamente viene generata una
	 * EbtablesException.
	 * 
	 * @throws IOException
	 * @throws SSHCommandException
	 * @throws InterruptedException
	 * @throws TrafficControlException 
	 * @throws EbtablesException
	 */
	public void enforce() throws IOException, SSHCommandException, InterruptedException, EbtablesException{
		
		log.info("Enforce Topology...");
		
		initialize();
		
		FileWriter w = new FileWriter(file);
		BufferedWriter bw=new BufferedWriter (w);
		
		for (Hub hub : hubs){
			
			log.info("Try to Enforce Topology on Hub name: "+ hub.getName());
						
			
			ArrayList<DistributedCommand> distributedCommands = new ArrayList<DistributedCommand>();
			distributedCommands = hub.getEbtablesAddRules();
			
			for(DistributedCommand command : distributedCommands)
			{			
				SSHConnectionHandler handler = SSHConnectionHandler.createSSHConnectionHandler(
													command.getPhysicalNode().getControlIP(),
													command.getPhysicalNode().getControlPort(),
													"root","nepadmpwd");
				
				/* 
				 * I comandi da eseguire vengono scritti nel file undo.txt prima che essi vengano
				 * eseguiti poichè in caso contrario un crash tra l'esecuzione del comando e la 
				 * scrittura nel log lascerebbe i dati in uno stato inconsistente. Bisogna però 
				 * ricordarsi, nella procedura undo(), che l'ultimo comando, avendo generato il codice
				 * di errore, non è stato eseguito sul server e quindi non va invertito.
				 */
				bw.write(command.getPhysicalNode().getControlIP() + " " +
	    				command.getPhysicalNode().getControlPort() + " " + "root nepadmpwd\n");
				/*
				 * A ebtables rule can be removed by specifying the complete rule as it would have 
				 * been specified when it was added. Only the first encountered rule that is the same
				 * as this specified rule, in other words the matching rule with the lowest (positive)
				 * rule number, is deleted.
				 */ 
				String deleteCommand = command.getCommand().replaceFirst(" -A "," -D ");
				bw.write(deleteCommand + "\n");
				log.debug("Write Undo Log!");

				bw.flush();
				int exitCode = handler.sendCommand(command.getCommand());
				
			    
			    if(exitCode != 0){
					bw.close();
					w.close();
					
					log.error("Error executing command. Standard Error is >>> " +
										"ExitCode: " + exitCode + " " + handler.getStdErrString());
					undo();
					file.delete();
					return;
				}
					
			}
			
			log.info("...Topology enforced on Hub name: "+ hub.getName() +"!");
		
		}//fine ciclo for su hubs

		bw.close();
		w.close();
		file.delete();
		
		log.info("Topology enforced!");
	}
	
	/**
	 * Distrugge la topologia descritta dalla lista di Hub inviando i comandi ebtables opportuni
	 * ai server fisici opportuni. Se solo un comando non viene eseguito correttamente viene
	 * generata una EbtablesException.
	 * 
	 * @throws IOException
	 * @throws SSHCommandException
	 * @throws InterruptedException
	 * @throws EbtablesException
	 */
	public void destroy() throws IOException, SSHCommandException, InterruptedException{
		
		log.info("Try to destroy Ebtables Topology...");
		
		

		for (Hub hub : hubs){
			ArrayList<DistributedCommand> distributedCommands = new ArrayList<DistributedCommand>();
			distributedCommands = hub.getEbtablesDeleteRules();
			
			for(DistributedCommand command : distributedCommands)
			{			
				SSHConnectionHandler handler = SSHConnectionHandler.createSSHConnectionHandler(
													command.getPhysicalNode().getControlIP(),
													command.getPhysicalNode().getControlPort(),
													"root","nepadmpwd");
				int exitCode=handler.sendCommand(command.getCommand());
				if(exitCode != 0){
					log.warn("Delete command failed. Standard Error is >>> " +
							"ExitCode(" + exitCode + "): " + handler.getStdErrString());
				} 
					
			}			
		}
		
		log.info("Ebtables Topology destroyed!");
		
	}
	
	/**
	 * Resetta le tabelle di ebtables su tutti i server fisici coninvolti nella topologia 
	 * determinata dagli hub. Lo stato iniziale di ebtables consiste nell'impostare ad ACCEPT
	 * tutte le tabelle e ad eliminare tutte le regole.
	 * 
	 * @throws IOException
	 * @throws SSHCommandException
	 * @throws InterruptedException
	 * @throws EbtablesException
	 */
	public void reset() throws IOException, SSHCommandException, 
														InterruptedException, EbtablesException{
		
		log.info("Try to reset all Ebtables Rules...");
		
		ArrayList<Node> nodes = this.getPhysicalNode();
		
		for(Node node : nodes)
		{
			SSHConnectionHandler handler = SSHConnectionHandler.createSSHConnectionHandler(
												node.getControlIP(),node.getControlPort(),
												"root", "nepadmpwd");
			
			if (	handler.sendCommand(Command.getEbtablesForwardPolicyRESET()) == 0 &&
					handler.sendCommand(Command.getEbtablesForwardRESET()) == 0 &&
					handler.sendCommand(Command.getEbtablesPostroutingRESET()) == 0 )
				
				log.info("Reset all Ebtables Rules on IP server "
								+ node.getControlIP().getAddress()+ ":"+ node.getControlPort());
			else
				throw new EbtablesException();
			
		}

	}
	
	/**
	 * Restituisce la lista di tutti i nodi Fisici sui quali sono istanziate le VM con almeno una 
	 * scheda di rete virtuale connessa, tramite un link, alla lista di Hub.
	 * 
	 * @return {@link ArrayList<{@link Node}>
	 */
	private ArrayList<Node> getPhysicalNode(){
	
		ArrayList<Node> physicalNodes = new ArrayList<Node>();
		
		for (Hub hub : hubs){
			
			ArrayList<Node> tmp  = hub.getPhysicalNode();
			
			for (Node node : tmp){
				if ( physicalNodes.indexOf(node) == -1 )
					physicalNodes.add(node);
			}
		}
		return physicalNodes;
	}
	
	/**
	 * Inizializza i nodi Fisici coinvolti nella topologia determinata dagli Hub. Tale operazione
	 * contiene due comandi idempotenti per l'impostazione della politica di DROP sulla catena FORWARD 
	 * della tabella filter, e la politica ACCEPT sulla catena POSTROUTING della tabella NAT. Qeusti comandi
	 * sono sempre eseguiti poichè non alterano la consistenza di una precedente configurazione. Una terza 
	 * regola consiste nell'abilitare il protocollo ARP. Essendo questa una regola non idempotente
	 * viene verificata la sua presenza sulle macchine server coinvolte nella topologia. 
	 *  
	 * @throws EbtablesException
	 */
	private void initialize() throws EbtablesException{
		
		ArrayList<Node> nodes = this.getPhysicalNode();
		
		for(Node node : nodes)
		{
			try 
			{
				SSHConnectionHandler handler = SSHConnectionHandler.createSSHConnectionHandler(
										node.getControlIP(),node.getControlPort(),
										"root", "nepadmpwd");
				
				if	(	handler.sendCommand(Command.getEbtablesForwardPolicyDROP())!=0 ||
						handler.sendCommand(Command.getEbtablesPostroutingPolicyACCEPT())!=0 ){
					log.error("Error in policy setting");
					throw new EbtablesException();
				}
					
				
				if(handler.sendCommand(Command.getEbtablesForwardList())==0){
					String stdOut = handler.getStdOutString();
					if ( stdOut.indexOf("-p ARP -j ACCEPT") == -1){
						if (handler.sendCommand(Command.getEbtablesForwardArpACCEPT()) !=0)
							throw new EbtablesException();
					}else
						log.warn("ARP rule already exists on IP server " +
								node.getControlIP().getAddress()+ ":"+ node.getControlPort());
						
				}else
					throw new EbtablesException();
				
				handler.dispose();
				
			} catch (IOException e) {
				log.error("Server Initialization Failed to IP: " 
						+ node.getControlIP().getAddress()+ ":" 
						+ node.getControlPort() + " !!");
			} catch (SSHCommandException e) {
				log.error("Server Initialization Failed to IP: " 
						+ node.getControlIP().getAddress()+ ":" 
						+ node.getControlPort() + " !!");
			}
		}
	}
	
	
	
	private void undo() throws IOException, EbtablesException, SSHCommandException{
		
		log.info("Start procedure UNDO");
        FileInputStream fis = new FileInputStream(file);

        byte[] content = new byte[fis.available()];
        fis.read(content);
         
        String data = new String(content);
        String[] split = data.split(System.getProperty("line.separator"));
        
        if((split.length % 2)!=0)
        {
        	log.error("Wrong number of entries in the file undo.txt!");
        	throw new EbtablesException("Wrong number of entries in the file undo.txt!");
        }
        else 
        if ((split.length-2) > 0){
        	
        	boolean isCommand = true;
        	String command = "";
        	for (int i = (split.length-2);i>0;i--){
        		
        		if(isCommand)
        		{
        			command = split[i-1];
        			isCommand = false;
        		}else
        		{		
        			StringTokenizer st = new StringTokenizer(split[i-1]);
        			SSHConnectionHandler handler;
					try {
						handler = SSHConnectionHandler.createSSHConnectionHandler(
											new IPAddress(4,st.nextToken(),"255.255.255.0"),
											Integer.parseInt(st.nextToken()),
											st.nextToken(),
											st.nextToken());
					} catch (NumberFormatException e) {
						e.printStackTrace();
						throw new EbtablesException();
					} catch (IllegalValueException e) {
						e.printStackTrace();
						throw new EbtablesException();
					}
        			
        			if( handler.sendCommand(command) != 0 ){
        				log.error("...UNDO procedure failure!");
        				throw new EbtablesException(handler.getStdErrString());
        			}
        			
        			isCommand = true;        			
        		}
        		
        	}//fine ciclo for
        	
        }else{
        	log.warn("...no rule included and therefore no rule to be restored!");
        	return;
        }
        log.info("UNDO procedure completed successfully!");
	}
	

}
